﻿using System;
using System.Collections;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Text;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using KeyEventArgs=System.Windows.Forms.KeyEventArgs;
using MouseEventArgs=System.Windows.Forms.MouseEventArgs;

namespace HexControl
{
    /// <summary>
    /// 사용자가 Hex 컨트롤을 사용해서 이벤트가 발생했을 때
    /// 이를 처리해주기 위한 이벤트 핸들러
    /// Hex 컨트롤을 사용하는 객체는 이 이벤트 핸들러를 이용해서
    /// 적절하게 이벤트에 대응해 줘야 한다.
    /// </summary>
    public interface IHexControlEventHandler
    {
        void ClickedOffset(int offset);
    }

    public partial class HexControl : UserControl
    {
        private IHexControlEventHandler _EventHandler;

        #region 멤버 변수 선언

        ///////////////////////////////////
        // GUI 관련된 객체들
        // 이 객체들은 추후에 모두 옵션창을 통해서
        // 변경할 수 있도록 개선할 예정이다.
        // 이 필드들은 옵션으로 선택이 가능한 것들과
        // 그렇지 않은 것들을 구분해서 처리한다.
        //
        ///////////////////////////////////
        
        private Font _Font;                                     // 폰트의 종류
        private int _FontSize;                                  // 폰트의 크기

        private SolidBrush _ForegroundBrushOfAddress;           // 주소 영역의 전경색
        private SolidBrush _BackgroundBrushOfAddress;           // 주소 영역의 배경색
        private SolidBrush _ForegroundBrushOfHex;               // Hex 영역의 전경색
        private SolidBrush _BackgroundBrushOfHex;               // Hex 영역의 배경색
        private SolidBrush _ForegroundBrushOfAscii;             // 아스키 영역의 전경색
        private SolidBrush _BackgroundBrushOfAscii;             // 아스키 영역의 배경색
        private SolidBrush _ForegroundBrushOfSelected;          // 선택된 영역의 전경색
        private SolidBrush _BackgroundBrushOfSelected;          // 선택된 영역의 배경색
        private SolidBrush _BackgroundBrushOfSelectedOutline;   // 선택된 영역의 테두리 라인 색
        private Pen _PenSelected;                               // 선택된 영역의 테두리 선 크기

        // 아래 필드들은 옵션에서 변경이 불가능한 것들이다.
        //
        private int _SelectedOffset;                            // 선택된 영역의 오프셋
        private int _SelectedLength;                            // 선택된 영역의 길이

        private bool _IsSelected;                                // 선택된 영역이 있는가?
        private float _LeftOffset;                              // 왼쪽 여백의 크기(픽셀 값)
        private float _TopOffset;                               // 위쪽 여백의 크기(픽셀 값)
        private float _BottomOffset;                            // 아래쪽 여백의 크기(픽셀 값)

        private float _Height;                                  // 전체 높이(픽셀 값)
        private int _VisibleLine;                               // 현재 화면에 보이는 줄 번호

        private Region _TooltipRegion;                          // 툴팁을 보여줄 때 계산하는 영역
        private string _TooltipText;                            // 툴팁으로 보여줄 내용

        private float _WidthOfAddress;                          // 주소 영역의 너비
        private float _WidthOfHex;                              // Hex 값 영역의 너비
        private float _WidthOfAscii;                            // 아스키 영역의 너비(실제로는 전체 영역으로 계산되기 때문에 사용되지 않음)
        private float _LineHeight;                              // 한 줄의 높이
        private float _SectionIndent;                           // 영역을 선택했을 때 데이터 사이에 얼마만큼의 인덱스가 필요한지를 계산

        private int _StartAddress;                              // 화면에 보여지는 시작 주소
        private int _CaretOffset;                               // 캐럿이 전체 스트림 상에서 어느 위치에 존재하는지를 나타낸다. 이 값을 편집시에 사용한다.
        private bool _IsCaretLowWord;                           // 한 바이트를 2문자로 표현하기 때문에, 캐럿에 상위 Word인지, 하위 Word인지를 구분해야 한다.
        private int _CaretPosX;                                 // 좌표 상에서의 캐럿의 위치
        private int _CaretPosY;                                 // 좌표 상에서의 캐럿의 위치
        private bool _KeyDownedFirstTime;                       // 키 값을 연속적으로 처리해주기 위한 변수


        ///////////////////////////////////
        // 스트림 관련 객체들
        ///////////////////////////////////
        private byte[] _Stream;                                 // 스트림 값
        private int _StreamLength;                              // 스트림의 전체 길이
        private int _LineSize;                                  // 한 줄당 표현할 데이터의 길이
        private int _NumberOfLine;                              // 전체 스트림을 표현하기 위해 필요한 줄 정보
        private int _StreamOffset;                              // 현재 화면에 보이는 첫번째 스트림의 오프셋

        #endregion

        public HexControl()
        {
            _Stream = null;
            _SectionIndent = 2;
            _LeftOffset = 0;
            _TopOffset = 20;
            _BottomOffset = 10;
            _VisibleLine = 0;
            _StartAddress = 0;
            _LineSize = 0x10;
            _NumberOfLine = 0;
            _StreamOffset = 0x00;

            _CaretOffset = -1;
            _IsCaretLowWord = false;
            _CaretPosX = 0;
            _CaretPosY = 0;
            _KeyDownedFirstTime = true;


            _ForegroundBrushOfAddress = new SolidBrush(Color.FromArgb(21, 64, 48));
            _ForegroundBrushOfHex = new SolidBrush(Color.FromArgb(0, 0, 0));
            _ForegroundBrushOfAscii = new SolidBrush(Color.FromArgb(255, 255, 255));
            _BackgroundBrushOfAddress = new SolidBrush(Color.FromArgb(196, 72, 72));
            _BackgroundBrushOfHex = new SolidBrush(Color.FromArgb(170, 203, 126));
            _BackgroundBrushOfAscii = new SolidBrush(Color.FromArgb(61, 117, 201));
            _ForegroundBrushOfSelected = new SolidBrush(Color.FromArgb(75, 150, 1));
            _BackgroundBrushOfSelected = new SolidBrush(Color.FromArgb(255, 255, 210));
            _PenSelected = new Pen(Color.FromArgb(255, 0, 0), 2);
            _BackgroundBrushOfSelectedOutline = new SolidBrush(Color.FromArgb(255, 0, 0));
            _FontSize = 10;
            SetFont("Courier New", _FontSize);

            _TooltipRegion = new Region();
            _TooltipRegion.MakeEmpty();
            _TooltipText = "";

            _IsSelected = true;
            _Height = this.Height - _TopOffset - _BottomOffset;

            InitializeComponent();

            // LearnFromCode: Double Buffering
            // Windows Forms는 더블 버퍼링을 기본적으로 지원하기 때문에
            // MFC처럼 임시 비트맵과 GDI를 만들 필요가 없다.
            // .NET 정말 좋은데~!!
            //
            SetStyle(ControlStyles.AllPaintingInWmPaint, true);
            SetStyle(ControlStyles.DoubleBuffer, true);
        }

        public void SetEventHandler(IHexControlEventHandler eventHandler)
        {
            Debug.Assert(eventHandler != null);
            _EventHandler = eventHandler;
        }

        /// <summary>
        /// 데이터 스트림을 저장하고 사용자 인터페이스 관련된 부분(스크롤)을 초기화한다.
        /// </summary>
        /// <param name="data">설정할 바이트 스트림</param>
        /// <param name="length">설정할 바이트 스트림의 길이</param>
        public void SetStream(byte[] data, int length)
        {
            Debug.Assert(length != 0);
            Debug.Assert(data != null);

            // 이전에 저장되어 있던 영역을 Dispose한다.
            //
            if (_Stream != null ) {
                _Stream = null;
            }

            // 새로운 영역을 할당하고 저장한다.
            //
            _Stream = new byte[length];
            for (int i = 0 ; i < length ; i++) {
                _Stream[i] = data[i];
            }
            
            // 새로운 스트림에 대한 각종 변수들을 초기환다.
            //
            _StreamLength = length;

            // 스트림의 길이를 토대로 전체 줄 수를 계산한다.
            // 이때, 마지막 줄은 남은 영역이 있을 때 추가한다.
            //
            _NumberOfLine = _StreamLength / _LineSize;
            _NumberOfLine += (_StreamLength%_LineSize == 0 ? 0 : 1);      // 남은 데이터가 있는가? 그렇다면 한줄 더 추가
            _VisibleLine = (int)((_Height) / _LineHeight);


            // 스크롤 바 정보를 새로 갱신한다.
            //
            vScrollBar.Minimum = 0;
            vScrollBar.SmallChange = (int)1;
            vScrollBar.LargeChange = (int)_Height / 10;
            if (_VisibleLine < _NumberOfLine) {
                vScrollBar.Maximum = (int)(_NumberOfLine + _Height / 20);
            }


            // 선택 영역을 새로 초기화한다.
            //
            _IsSelected = false;
            _SelectedOffset = 0x00;
            _SelectedLength = 0x00;


            // 화면 업데이트!
            //
            Invalidate();
            Update();
        }

        /// <summary>
        /// 사용자가 선택한 내용을 화면 상에서 선택한다.
        /// </summary>
        /// <param name="path">선택된 노드의 전체 경로</param>
        /// <param name="name">이름</param>
        /// <param name="comment">주석</param>
        /// <param name="offset">오프셋</param>
        /// <param name="size">크기</param>
        public void SelectRegion(string path, string name, string comment, int offset, int size)
        {
            Debug.Assert((offset + size ) < _StreamLength);
            Debug.Assert(offset >= 0);

            _SelectedOffset = offset;
            _SelectedLength = size;

            // 현재 화면에 보이는 줄 번호를 계산한다.
            // 만약 선택된 영역이 화면에서 보이지 않는다면,
            // 해당 위치로 자동 스크롤을 수행한다.
            //
            int select_line = offset/_LineSize;
            int max_visible_line = (offset + size)/_LineSize;
            int current_line = _StreamOffset/_LineSize;

            if (select_line < current_line) {
                current_line = select_line;
            }
            else if ( current_line + _VisibleLine < max_visible_line) {
                current_line = select_line;
            }

            // 스트림 오프셋을 현재 선택된 내용의 첫번째 줄로 계산하고
            // 툴팁 정보를 설정한다.
            //
            _StreamOffset = current_line * _LineSize;
            _TooltipText = "Path: " + path + "\nName: " + name + "\nSize: " + size + ", 0x" + size.ToString("X") + " bytes\nOffset: " + offset + ", 0x" + offset.ToString("X");

            vScrollBar.Value = current_line;
            vScrollBar.Select();
            _IsSelected = true;

            // 화면 업데이트
            Invalidate();
            Update();
        }

        /// <summary>
        /// 이 코드는 <code>bin</code>매개 변수를 문자로 변경한다.
        /// </summary>
        /// <param name="bin">문자로 변경할 HEX 값</param>
        /// <returns></returns>
        private char BINTOCHAR(byte bin)
        {
            // 화면에 표현이 가능한 ASCII 값이 아닌 경우에는
            // 간단히 .으로 표현한다.
            //
            if ( bin < 0x30 || bin > 'z' ) {
                return '.';
            } 
            else {
                return (char) bin;
            }
        }

        public void ReadStream()
        {
            throw new System.NotImplementedException();
        }

        /// <summary>
        /// 새로운 폰트를 설정한다.
        /// </summary>
        /// <param name="fontName">폰트 이름</param>
        /// <param name="fontSize">폰트 크기</param>
        public void SetFont(String fontName, int fontSize)
        {
            _Font = new Font(fontName, fontSize, FontStyle.Bold, GraphicsUnit.Pixel);
        }

        /// <summary>
        /// HEX 컨트롤의 화면을 업데이트한다.
        /// 이 메서드에서 화면에 보이는 모든 내용을 갱신한다.
        /// 이 때 업데이트하는 순서를 잘 지켜줘야 모든 내용이 제대로 표시된다.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void HexControl_Paint(object sender, PaintEventArgs e)
        {
            Native.HideCaret(this.Handle);

            // 품질 향상을 위한 옵션 설정
            //
            e.Graphics.InterpolationMode = InterpolationMode.HighQualityBilinear;
            e.Graphics.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;

            // 화면에 모든 내용들이 제대로 표시되려면 아래의 함수 호출 순서가 굉장히 중요한다.
            // 그럴 일은 없겠지만, 혹시라도 함수의 호출 순서를 바꿔야 한다면
            // 굉장히 신중해야 한다.
            //
            DrawAddress(e.Graphics);                                        // 주소 영역 그리고,
            DrawHex(e.Graphics);                                            // HEX 영역 그리고,
            DrawASCII(e.Graphics);                                          // 아스키 영역 그리고,
            DrawSelection(e.Graphics, _SelectedOffset, _SelectedLength);    // 마지막으로 선택된 영역을 그린다.

            // 화면을 갱신할 때마다
            // 캐럿을 항상 다시 그려줘야 한다.
            //
            SetCaretPosFromOffset(_CaretOffset, _IsCaretLowWord, e.Graphics);
            Native.ShowCaret(this.Handle);
        }

        /// <summary>
        /// 주소와 HEX, 아스키 영역의 크기를 폰트를 기준으로 구한다.
        /// 주소 영역부터 구한 다음, HEX와 아스키 영역을 차례대로 구한다.
        /// 이 함수는 새로운 폰트가 설정될 때마다 호출되어야 한다.
        /// </summary>
        /// <param name="g">그래픽 객체</param>
        private void GetMeasure(Graphics g)
        {
            // 1. 주소 영역 계산
            //
            SizeF size = g.MeasureString("00000000", _Font);
            _WidthOfAddress = (int)size.Width;
            _LineHeight = size.Height;

            // 2. HEX 영역
            // 주소 영역 + 여백 크기
            //
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < (_LineSize - 1); i++) {
                sb.Append("00 ");
            }
            sb.Append("00");
            size = g.MeasureString(sb.ToString(), _Font);
            _WidthOfHex = size.Width;
            sb = null;

            // 3. 아스키 영역
            // HEX 영역 + 여백 크기("00"이 들어갈만한 공간)
            sb = new StringBuilder();
            for (int i = 0; i < _LineSize; i++) {
                sb.Append("00");
            }
            size = g.MeasureString(sb.ToString(), _Font);
            _WidthOfAscii = size.Width;
            sb = null;
        }

        /// <summary>
        /// 주소 영역을 그린다.
        /// 약간 복잡한 계산이 요구된다.
        /// </summary>
        /// <param name="g"></param>
        private void DrawAddress(Graphics g)
        {
            // 주소 영역을 그리는 과정은 다음과 같다.
            // 1. 배경색을 채운다.
            // 2. 문자를 그린다.
            // 3. 마지막에 룰러를 그린다.
            //

            // 1. 배경색을 채운다.
            //
            g.FillRectangle(_BackgroundBrushOfAddress, _LeftOffset + _SectionIndent, _TopOffset, _WidthOfAddress - (_SectionIndent * 2), _Height);

            // 2. 문자를 그린다.
            //
            int offset = _StartAddress + _StreamOffset;
            int remained = _StreamLength - _StreamOffset;
            int NumberOfLine = remained / _LineSize;
            NumberOfLine += (remained % _LineSize == 0 ? 0 : 1);
            NumberOfLine = (_VisibleLine > NumberOfLine ? NumberOfLine : _VisibleLine);

            for (int j = 0; j < NumberOfLine; j++) {
                String str =offset.ToString("X8");
                g.DrawString(str, _Font, _ForegroundBrushOfAddress, new PointF(_LeftOffset, _TopOffset + j * _LineHeight));
                offset += _LineSize;
                str = null;
            }

            // 3. 룰러를 그린다.
            //
            g.DrawString("Address",_Font,_ForegroundBrushOfAddress, new PointF(_LeftOffset + (_WidthOfAddress/8), _TopOffset - _LineHeight));
        }

        /// <summary>
        /// HEX 영역을 그린다.
        /// </summary>
        /// <param name="g"></param>
        private void DrawHex(Graphics g)
        {
            // 주소 영역을 그리는 과정은 다음과 같다.
            // 1. 배경색을 채운다.
            // 2. 내용을 그린다.
            // (선택된 영역은 나중에 따로 그리기 때문에, 이 함수에는 포함되어 있지 않다.)
            // 3. 마지막에 룰러를 그린다.
            //
            
            // 1. 배경색을 채운다.
            //
            float OffsetX = _WidthOfAddress;
            g.FillRectangle(_BackgroundBrushOfHex, OffsetX + _SectionIndent, _TopOffset, _WidthOfHex - (_SectionIndent * 2), _Height);
            int Offset = _StreamOffset;

            // 2. 내용을 그린다.
            //
            int Remained = _StreamLength - _StreamOffset;
            int NumberOfLine = Remained / _LineSize;
            int OneMoreLine = (Remained%_LineSize == 0 ? 0 : 1);
            if (_VisibleLine < NumberOfLine + OneMoreLine) {
                NumberOfLine = _VisibleLine;
                Remained = 0;
            }
            
            for (int i = 0;i < NumberOfLine; i++) {
                StringBuilder sb = new StringBuilder();

                for ( int j = 0 ;j < (_LineSize-1) ; j++ ) {
                    sb.Append(_Stream[Offset + j].ToString("X2") + " ");
                }

                sb.Append(_Stream[Offset + _LineSize-1].ToString("X2"));
                SizeF sizeTemp = g.MeasureString(sb.ToString(), _Font);
                g.DrawString(sb.ToString(), _Font, _ForegroundBrushOfHex, new PointF(OffsetX, _TopOffset + i * _LineHeight));
                Offset += _LineSize;
                sb = null;
            }

            StringBuilder sbRemained = new StringBuilder();
            Remained = (Remained%_LineSize);
            for (int k = 0; k < Remained; k++) {
                sbRemained.Append(_Stream[Offset + k].ToString("X2") + " ");
            }

            if (Remained != 0) {
                g.DrawString(sbRemained.ToString(), _Font, _ForegroundBrushOfHex, new PointF(OffsetX, _TopOffset + NumberOfLine * _LineHeight));    
            }
            
            sbRemained = null;
            
            // 3. 룰러를 그린다.
            //
            g.DrawString("00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F",_Font, _ForegroundBrushOfHex, new PointF(OffsetX, _TopOffset - _LineHeight));

        }

        /// <summary>
        /// 아스키 영역을 그린다.
        /// </summary>
        /// <param name="g"></param>
        private void DrawASCII(Graphics g)
        {
            // 아스키 영역을 그리는 과정은 다음과 같다.
            // 1. 배경색을 채운다.
            // 2. 내용을 그린다.
            // 3. 마지막에 룰러를 그린다.
            //

            // 1. 배경색을 채운다.
            //
            float OffsetX = _WidthOfHex + _WidthOfAddress;
            g.FillRectangle(_BackgroundBrushOfAscii, OffsetX + _SectionIndent, _TopOffset, _WidthOfHex - (_SectionIndent * 2), _Height);

            // 2. 내용을 그린다.
            //
            int Offset = _StreamOffset;
            int Remained = _StreamLength - _StreamOffset;
            int NumberOfLine = Remained / _LineSize;
            int OneMoreLine = (Remained % _LineSize == 0 ? 0 : 1);
            if (_VisibleLine < NumberOfLine + OneMoreLine)
            {
                NumberOfLine = _VisibleLine;
                Remained = 0;
            }

            for (int i = 0; i < NumberOfLine; i++)
            {
                StringBuilder sb = new StringBuilder();

                for (int j = 0; j < _LineSize; j++)
                {
                    sb.Append(BINTOCHAR(_Stream[Offset + j]));
                }
                g.DrawString(sb.ToString(), _Font, _ForegroundBrushOfAscii, new PointF(OffsetX, _TopOffset + i * _LineHeight));
                Offset += _LineSize;
                sb = null;
            }


            StringBuilder sbRemained = new StringBuilder();
            Remained = (Remained % _LineSize);
            for (int k = 0; k < Remained; k++)
            {
                sbRemained.Append(BINTOCHAR(_Stream[Offset + k]));
            }

            if (Remained != 0)
            {
                g.DrawString(sbRemained.ToString(), _Font, _ForegroundBrushOfAscii, new PointF(OffsetX, _TopOffset + NumberOfLine * _LineHeight));
            }

            sbRemained = null;

            // 3. 마지막에 룰러를 그린다.
            //
            g.DrawString("0123456789ABCDEF", _Font, _ForegroundBrushOfAscii, new PointF(OffsetX, _TopOffset - _LineHeight));
        }


        /// <summary>
        /// 선택된 영역을 그린다.
        /// </summary>
        /// <param name="g">그래픽 객체</param>
        /// <param name="selectedOffset">선택된 영역의 스트림 오프셋</param>
        /// <param name="selectedLength">선택된 영역의 길이</param>
        private void DrawSelection(Graphics g, int selectedOffset, int selectedLength)
        {
            // 선택된 영역은 총 3가지 단계로 그려야 한다.
            // 우선 선택된 영역을 간단하게 그릴 수 있는 API는 존재하지 않는다.
            //
            // 1. 선택된 영역의 첫번째 줄의 영역을 계산한다.
            // 2. 두번째 줄부터 (마지막 줄-1) 까지의 영역을 계산한다.
            // 3. 마지막 줄의 영역을 계산한다.
            // 4. 모든 영역에 대해서 테두를 계산하고
            // 5. 테두리와 배경색을 칠한 다음,
            // 6. 해당 영역의 문자를 그 위에 덮어서 그린다.
            //

            float OffsetX = _WidthOfAddress;
            
            // 선택된 영역을 검증한다.
            //
            if (_IsSelected && (_StreamOffset <= selectedOffset + selectedLength) ) {
                int CurrentOffset = 0;
                int RemainedLength = 0;

                // 선택된 영역과 현재 선택 가능한 영역의 크기를 계산하여
                // 선택된 영역의 길이와 오프셋을 계산한다.
                //
                if ( _StreamOffset + _VisibleLine * _LineSize <= selectedOffset ) {
                    RemainedLength = 0;
                }
                else if (_StreamOffset <= selectedOffset) {
                    if ((_StreamOffset + _VisibleLine * _LineSize) >= (selectedOffset) &&
                        (_StreamOffset + _VisibleLine * _LineSize) <= (selectedLength + selectedOffset)) {
                        CurrentOffset = selectedOffset;
                        RemainedLength = _StreamOffset + _VisibleLine * _LineSize - selectedOffset;
                    }
                    else {
                        CurrentOffset = selectedOffset;
                        RemainedLength = selectedLength;
                    }
                }
                else if (_StreamOffset <= (selectedOffset + selectedLength)) {
                    CurrentOffset = _StreamOffset;
                    RemainedLength = selectedLength - (_StreamOffset - selectedOffset);
                }

                // 선택된 영역이 있는가? (RemainedLength > 0)
                //
                if ( RemainedLength > 0) {
                    ArrayList arrString = new ArrayList();
                    ArrayList arrRect = new ArrayList();

                    // 1. 선택된 영역의 첫번째 줄의 영역을 계산한다.
                    // 첫번째 줄의 영역을 계산하는 방법
                    // 선택된 첫번째 문자 이전 까지의 길이를 구한후,
                    // 전체 영역에서 구한 값을 뺀다.
                    //
                    System.Drawing.Region r = new Region();
                    r.MakeEmpty();
                    GraphicsPath gp = new GraphicsPath(FillMode.Winding);

                    // index 앞까지의 문자 길이를 구한다.
                    //
                    StringBuilder strLeftIndent = new StringBuilder();
                    int i = 0;
                    int current_line_offset = CurrentOffset / _LineSize * _LineSize;

                    for (i = current_line_offset; i < CurrentOffset; i++) {
                        strLeftIndent.Append(_Stream[i].ToString("X2") + " ");
                    }
                    SizeF sizeOfLeftIndent = g.MeasureString(strLeftIndent.ToString(), _Font);

                    StringBuilder strSelected = new StringBuilder();

                    int len = 0;
                    if (CurrentOffset + RemainedLength >= (current_line_offset + _LineSize)) {
                        len = current_line_offset + _LineSize;
                    }
                    else {
                        len = CurrentOffset + RemainedLength;
                    }

                    int count = 0;
                    for (; i < len; i++) {
                        count++;
                        strLeftIndent.Append(_Stream[i].ToString("X2") + " ");
                        strSelected.Append(_Stream[i].ToString("X2") + " ");
                    }

                    SizeF size1 = g.MeasureString(strLeftIndent.ToString(), _Font);
                    SizeF sizeSelection = g.MeasureString(strSelected.ToString(), _Font);

                    RectangleF rect = new RectangleF(OffsetX + (size1.Width - sizeSelection.Width),
                                                     _TopOffset + (CurrentOffset - _StreamOffset) / _LineSize * _LineHeight,
                                                     sizeSelection.Width,
                                                     _LineHeight
                        );

                    arrString.Add(strSelected);
                    arrRect.Add(rect);

                    r.Union(rect);
                    gp.AddRectangle(rect);

                    RemainedLength -= count;
                    CurrentOffset += count;

                    // 2. 두번째 줄부터 (마지막 줄-1) 까지의 영역을 계산한다.
                    //

                    if (RemainedLength / _LineSize > 0) {
                        int remained_line = RemainedLength / _LineSize;
                        for (int j = 0; j < remained_line; j++) {
                            RectangleF rect2 = new RectangleF(OffsetX,
                                                              _TopOffset + (CurrentOffset - _StreamOffset) / _LineSize * _LineHeight,
                                                              _WidthOfHex,
                                                              _LineHeight);

                            StringBuilder str4 = new StringBuilder();
                            for (int k = 0; k < _LineSize; k++) {
                                str4.Append(_Stream[k + CurrentOffset].ToString("X2") + " ");
                            }
                            arrRect.Add(rect2);
                            arrString.Add(str4);

                            r.Union(rect2);
                            gp.AddRectangle(rect2);

                            CurrentOffset += _LineSize;
                            RemainedLength -= _LineSize;
                        }

                    }

                    // 3. 마지막 줄의 영역을 계산한다.
                    //

                    StringBuilder str = new StringBuilder();
                    StringBuilder strLast = new StringBuilder();

                    for (i = 0; i < RemainedLength; i++) {
                        str.Append(_Stream[CurrentOffset + i].ToString("X2") + " ");
                        strLast.Append(_Stream[CurrentOffset + i].ToString("X2") + " ");
                    }
                    SizeF size2 = g.MeasureString(str.ToString(), _Font);

                    RectangleF rect1 = new RectangleF(OffsetX,
                                                      _TopOffset + (CurrentOffset - _StreamOffset) / _LineSize * _LineHeight,
                                                      size2.Width,
                                                      _LineHeight);

                    arrString.Add(strLast);
                    arrRect.Add(rect1);

                    r.Union(rect1);
                    gp.AddRectangle(rect1);

                    // 테두리 영역을 구하기 위해서 영역을 확장하기 전에
                    // 툴팁을 표시하기 위한 영역을 계산한다.
                    //
                    _TooltipRegion.Dispose();
                    _TooltipRegion = r.Clone();

                    // 4. 모든 영역에 대해서 테두리를 계산하고
                    //
                    gp.Widen(_PenSelected);

                    // 5. 테두리와 배경색을 칠한 다음,
                    // 
                    g.FillPath(_BackgroundBrushOfSelectedOutline, gp);

                    // 6. 해당 영역의 문자를 그 위에 덮어서 그린다.
                    //
                    g.FillRegion(_BackgroundBrushOfSelected, r);
                    for (int m = 0; m < arrString.Count; m++) {
                        g.DrawString(((StringBuilder)arrString[m]).ToString(), _Font, _ForegroundBrushOfSelected, new PointF(((RectangleF)arrRect[m]).Left, ((RectangleF)arrRect[m]).Top));
                    }

                    // 사용한 모든 자원을 해제한다.
                    //
                    gp = null;
                    r = null;
                }
                else
                {
                    // 선택된 영역이 없을 때에는
                    // 선택된 영역에 대한 툴팁을 없앤다.
                    //
                    _TooltipRegion.MakeEmpty();
                }
            }
        }

        private Color RGB(int p, int p_2, int p_3)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Hex 컨트롤이 활성화 되었을 때 호출되는 메서드
        /// 이 메서드에서는 캐럿 관련된 내용을 처리한다.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void HexControl_Enter(object sender, EventArgs e)
        {
            Native.DestroyCaret();
            Native.CreateCaret(this.Handle, IntPtr.Zero, 6, 12);
        }

        /// <summary>
        /// 컨트롤이 포커스를 잃게 되면
        /// 캐럿을 삭제한다.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void HexControl_Leave(object sender, EventArgs e)
        {
            Native.HideCaret(this.Handle);
            Native.DestroyCaret();
        }

        /// <summary>
        /// 스트림의 오프셋 값을 기준으로 해당 위치에 캐럿을 표시한다.
        /// </summary>
        /// <param name="offset">스트림 오프셋</param>
        /// <param name="isLowbit">해당 오프셋 바이트 중 하위 Word에 캐럿을 그릴 것인지를 지정한다.</param>
        /// <param name="g">그래픽 객체</param>
        private void SetCaretPosFromOffset(int offset, bool isLowbit, Graphics g)
        {
            if (_Stream == null ||  offset < 0 )
                return;

            int StartOffset = offset/_LineSize * _LineSize;
            float CursorY = ((StartOffset - _StreamOffset) / _LineSize) * _LineHeight + _TopOffset;
            float CursorX = 0;

            // Address 주소 공간 클릭
            //
            // 커서는 한 글자씩이기 때문에
            // 모든 문자열의 위치를 우선 저장한 다음
            // 한글자씩 비교해 보면서 캐럿의 위치를 찾아야 한다.
            //
            StringBuilder str = new StringBuilder();
            int TotalCount = (offset - StartOffset) * 2;
            float[] pos_list = new float[TotalCount];

            for (int i = 0; i < (offset - StartOffset); i++) {
                str.Append(_Stream[StartOffset + i].ToString("X2") + "_");
            }

            if ( isLowbit == true ) {
                str.Append(_Stream[offset].ToString("X2")[1]);
            }

            SizeF size = g.MeasureString(str.ToString(), _Font);
            CursorX = size.Width + _LeftOffset + _WidthOfAddress;

            Native.DestroyCaret();
            Native.CreateCaret(this.Handle, IntPtr.Zero, 6, 12);
            Native.SetCaretPos((int)CursorX, (int)CursorY);
            Native.ShowCaret(this.Handle);
            _CaretPosX = (int)CursorX;
            _CaretPosY = (int)CursorY;
        }

        /// <summary>
        /// 화면상의 위치를 기반으로 캐럿을 생성한다.
        /// 이 함수는 마우스 이벤트와 같은 곳에서 호출될 것이다.
        /// </summary>
        /// <param name="x">클라이언트 X 좌표</param>
        /// <param name="y">클라이언트 Y 좌표</param>
        /// <param name="isLowbit">해당 오프셋 바이트 중 하위 Word에 캐럿을 그릴 것인지를 지정한다.</param>
        /// <param name="g">그래픽 객체</param>
        /// <param name="MoveCaretPosition">캐럿 위치를 이동할 것인지를 지정한다.</param>
        /// <returns></returns>
        private int SetCaretPosFromPoint(int x, int y, ref bool isLowbit, Graphics g, bool MoveCaretPosition)
        {
            int CurrentLine = (int)((y - _TopOffset) / _LineHeight);
            float CursorY = CurrentLine * _LineHeight + _TopOffset;
            int StartOffset = CurrentLine * _LineSize + _StreamOffset;
            float CursorX = 0;
            int Offset = StartOffset;
            isLowbit = false;

            // Address 주소 공간 클릭
            //
            if (x <= _WidthOfAddress + _LeftOffset) {
                CursorX = _WidthOfAddress + _LeftOffset + _SectionIndent;
            }
            else if (x >= _WidthOfAddress + _WidthOfHex + _LeftOffset) {       // Hex 값 공간 클릭
                StringBuilder str = new StringBuilder();
                int max_char = Math.Min(StartOffset + _LineSize, _StreamLength) - StartOffset;

                for (int i = 0; i < max_char - 1; i++) {
                    str.Append(_Stream[i + StartOffset].ToString("X2") + "_");
                }

                SizeF size = g.MeasureString(str.ToString(), _Font);
                CursorX = size.Width + _LeftOffset + _WidthOfAddress;
                Offset += max_char;
            }
            else {
                // 커서는 한 글자씩이기 때문에
                // 모든 문자열의 위치를 우선 저장한 다음
                // 한글자씩 비교해 보면서 캐럿의 위치를 찾아야 한다.
                //
                int max_char = Math.Min(StartOffset + _LineSize, _StreamLength) - StartOffset;
                StringBuilder str = new StringBuilder();
                int total_count = max_char * 2;
                float[] pos_list = new float[total_count];

                string current_pos_hex = "";

                for (int i = 0; i < total_count; i++) {
                    if (i % 2 == 0) {
                        current_pos_hex = _Stream[StartOffset + i / 2].ToString("X2") + "_";
                        str.Append(current_pos_hex[0]);
                    }

                    if (i % 2 == 1) {
                        str.Append(current_pos_hex.Substring(1));
                    }

                    SizeF size = g.MeasureString(str.ToString(), _Font);
                    pos_list[i] = size.Width + _LeftOffset + _WidthOfAddress;
                }

                CursorX = _LeftOffset + _WidthOfAddress + _SectionIndent;
                for (int i = total_count - 1; i >= 0; i--) {
                    if (x > pos_list[i]) {
                        CursorX = pos_list[i];
                        Offset = (i+1) / 2 + StartOffset;
                        isLowbit = (i%2 == 0);
                        break;
                    }
                }
            }

            // b_move_caret_pos 가 true 인 경우에만
            // caret의 위치를 옮긴다. 그렇지 않은 경우에는
            // 마우스 포인터로부터 현재의 offset을 구한다.
            //
            if (MoveCaretPosition == true) {
                Native.DestroyCaret();
                Native.CreateCaret(this.Handle, IntPtr.Zero, 6, 12);
                Native.SetCaretPos((int)CursorX, (int)CursorY);
                Native.ShowCaret(this.Handle);
                _CaretPosX = (int)CursorX;
                _CaretPosY = (int)CursorY;
            }
            
            return Offset;
        }

        /// <summary>
        /// 마우스가 클릭되었을 때 호출된다.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void HexControl_MouseDown(object sender, MouseEventArgs e)
        {
            System.Drawing.Graphics g = this.CreateGraphics();
            bool IsLowbit = false; // ignored, SetCaretPosFromPoint에서 새로 설정됨

        
            if (e.Button == MouseButtons.Right ) {
                // 마우스 오른쪽 버튼이 눌렸을 때에는
                // 현재 위치로부터 선택 가능한 필드 값을 역으로 계산한다.
                //
                int CaretOffset = SetCaretPosFromPoint(e.X, e.Y, ref IsLowbit, g, false);
                _EventHandler.ClickedOffset(CaretOffset);
            }
            else {
                // 해당 영역을 편집하기 위해 캐럿을 생성한다.
                //
                _CaretOffset = SetCaretPosFromPoint(e.X, e.Y, ref IsLowbit, g, true);
                _IsCaretLowWord = IsLowbit;
                _KeyDownedFirstTime = true;
                _SelectedLength = 0;
                _SelectedOffset = 0;
            }

        }

        private void vScrollBar_Scroll(object sender, ScrollEventArgs e)
        {
            int Line = e.NewValue;
            _StreamOffset = Line*_LineSize;
            Refresh();
        }

        private void vScrollBar_Resize(object sender, EventArgs e)
        {
            _Height = this.Height - _TopOffset - _BottomOffset;
            _VisibleLine = (int)((_Height) / _LineHeight);
            Invalidate();
            Update();
        }

        private void HexControl_MouseMove(object sender, MouseEventArgs e)
        {
            // 마우스가 움직일 때, 선택된 영역 위에 위치하게 되면
            // 툴팁을 보여준다.
            //
            if (_TooltipRegion.IsVisible(e.X, e.Y) == true) {
                toolTip.Show(_TooltipText, this, e.X + 15, e.Y + 15);
            }
            else {
                toolTip.Hide(this);
            }
        }


        private void HexControl_MouseWheel(object sender, MouseEventArgs e)
        {
            // 휠 버튼으로 스크롤이 되면, 지정된 크기 만큼 화면을 스크롤 한다.
            // 특히 시작 위치와 마지막 위치를 잘 계산할 수 있어야 한다.
            //
            int numberOfTextLinesToMove = e.Delta * SystemInformation.MouseWheelScrollLines / 120;
            int numberOfPixelsToMove = numberOfTextLinesToMove * _FontSize;

            if (numberOfPixelsToMove != 0) {
                int line = vScrollBar.Value - numberOfTextLinesToMove;
                int total_line = _StreamLength/_LineSize;

                if (line < 0) {
                    line = 0;
                }
                else if (line > total_line) {
                    line = total_line;
                }

                vScrollBar.Value = line;
                _StreamOffset = line * _LineSize;
                Refresh();
            }

        }

        /// <summary>
        /// 오프셋에 있는 스트림의 바이트를 구한다.
        /// </summary>
        /// <param name="offset">스트림 오프셋</param>
        /// <returns><code>offset</code>이 가리키는 영역의 바이트 값</returns>
        public byte GetByte(int offset)
        {
            Debug.Assert(offset < _StreamLength);

            return _Stream[offset];
        }

        private void HexControl_Resize(object sender, EventArgs e)
        {
            Graphics g = this.CreateGraphics();

            GetMeasure(g);

            g.Dispose();
        }

        /// <summary>
        /// 키보드 키가 눌리게 되면 캐럿 위치에서 곧바로 편집 기능이 수행된다.
        /// 입력되는 값은 아스키 코드 값 정도이다.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void HexControl_KeyDown(object sender, KeyEventArgs e)
        {
            if ( (e.KeyValue >= 'A' && e.KeyValue <= 'F') ||
                (e.KeyValue >= 'a' && e.KeyValue <= 'f') ||
                (e.KeyValue >= '0' && e.KeyValue <= '9')) {
                if ( _KeyDownedFirstTime == true ) {
                    if (_CaretOffset == -1)
                        _CaretOffset = 0;
                    _SelectedOffset = _CaretOffset;
                    _SelectedLength = 1;
                    _KeyDownedFirstTime = false;
                }
                
                int value = 0;

                if ( e.KeyValue >= '0' && e.KeyValue <= '9' ) {
                    value = e.KeyValue - '0';
                }
                else if (e.KeyValue >= 'a' && e.KeyValue <= 'f') {
                    value = e.KeyValue - 'a' + 10;
                }
                else if (e.KeyValue >= 'A' && e.KeyValue <= 'F') {
                    value = e.KeyValue - 'A' + 10;
                }

                int CurrentOffset = _SelectedOffset + _SelectedLength - 1;

                // 상위 WORD인지 하위 WORD인지를 제대로 구분해서 스트림을 업데이트 해야한다.
                //
                if ( _IsCaretLowWord == true ) {
                    _Stream[CurrentOffset] = (byte) (_Stream[CurrentOffset] & 0xF0 | value);
                }
                else {
                    _Stream[CurrentOffset] = (byte)(value << 4 | _Stream[CurrentOffset] & 0x0F);
                }

                if ( (_IsCaretLowWord == true) && ((CurrentOffset) <= _StreamLength) ) {
                    CurrentOffset++;
                    _SelectedLength++;
                }

                _IsCaretLowWord = !_IsCaretLowWord;
                _CaretOffset = CurrentOffset;

                Invalidate();
                Update();
            }
        }

        private void HexControl_MouseDoubleClick(object sender, MouseEventArgs e)
        {

        }

        private void HexControl_MouseHover(object sender, EventArgs e)
        {

        }
    }

    class Native
    {
        [DllImport("user32.dll")]
        public static extern int CreateCaret(IntPtr hwnd, IntPtr hbm, int cx, int cy);

        [DllImport("user32.dll")]
        public static extern int DestroyCaret();

        [DllImport("user32.dll")]
        public static extern int SetCaretPos(int x, int y);

        [DllImport("user32.dll")]
        public static extern int ShowCaret(IntPtr hwnd);

        [DllImport("user32.dll")]
        public static extern int HideCaret(IntPtr hwnd);
    }
}
